Skip to content

Enhance OPcache configuration and documentation - #704

Merged
jaydrogers merged 8 commits into
release/webserver-improvements-and-fixesfrom
opcache-improvements
Sep 11, 2026
Merged

Enhance OPcache configuration and documentation#704
jaydrogers merged 8 commits into
release/webserver-improvements-and-fixesfrom
opcache-improvements

Conversation

@jaydrogers

@jaydrogers jaydrogers commented Sep 10, 2026

Copy link
Copy Markdown
Member

Why we created this PR

PHP_OPCACHE_ENABLE=0 is our default so your edits show up instantly in development. That stays the same.

The problem is PHP_OPCACHE_ENABLE=1. We call it the production setting, but behind it were PHP's stock defaults from 2013. OPcache checked every file for changes every two seconds, the memory sizes were too small for a modern Laravel or Symfony app, and PHP_OPCACHE_FORCE_RESTART_TIMEOUT never reached php.ini.

If you flip one switch for production, the settings behind it should be the ones you would pick yourself. This PR makes that true.

How we approached it

PHP_OPCACHE_ENABLE is a mode switch. 0 is development: OPcache off, mounted code updates instantly. 1 is production: code is built into the image, so cache it once and stop checking the filesystem.

For each setting we asked what PHP recommends for immutable containers, what a real app needs, and what it costs if we are wrong. Anything we could not justify stayed at PHP's default.

How to test

serversideup/php-dev:704-*

View the testing images →

docker run --rm -e PHP_OPCACHE_ENABLE=1 serversideup/php-dev:704-8.4-cli \
  php -r 'echo ini_get("opcache.validate_timestamps"), " ", ini_get("opcache.memory_consumption"), " ", ini_get("opcache.max_accelerated_files");'

Expect 0 256 20000 and a production mode notice in the startup banner.

What this PR does

Changed defaults (only when PHP_OPCACHE_ENABLE=1)

Variable Before After Why
PHP_OPCACHE_VALIDATE_TIMESTAMPS 1 0 Cache until restart. No filesystem checks per request.
PHP_OPCACHE_MEMORY_CONSUMPTION 128 256 Only used as files are cached, so it costs nothing until needed.
PHP_OPCACHE_INTERNED_STRINGS_BUFFER 8 16 Modern vendor trees exhaust 8 MB and force a cache restart. Reserved inside the 256 MB.
PHP_OPCACHE_MAX_ACCELERATED_FILES 10000 20000 Filament and Livewire apps overflow 10,000. Costs about 1 MB.

Kept on purpose: JIT off (little gain for web apps, conflicts with Xdebug), SAVE_COMMENTS=1 (Doctrine and PHPUnit need it), ENABLE_FILE_OVERRIDE=0 (lies about deleted files when timestamps are off).

New variables

Variable Default What it does
PHP_OPCACHE_PRELOAD "" Path to a preload script. A big win for Symfony apps.
PHP_OPCACHE_PRELOAD_USER "" User to preload as when the web server runs as root.

Fixes

  • PHP_OPCACHE_FORCE_RESTART_TIMEOUT now reaches php.ini. The ini line was commented out.
  • The PHP_OPCACHE_SAVE_COMMENTS doc said "remove comments" for a variable that keeps them.

Also

  • The startup banner explains production mode when OPcache is on.
  • scripts/test-image.sh runs every image with PHP_OPCACHE_ENABLE=1, so CI proves OPcache starts with the new sizes on Debian, Alpine, amd64, and arm64.
  • New PHP OPcache tuning guide and a Version 4 to 5 section in the migration guide. Updated the env spec, FrankenPHP, default configurations, and WordPress pages.

Compatibility

This is 5.0 because PHP_OPCACHE_VALIDATE_TIMESTAMPS=0 is breaking for one group: people who set PHP_OPCACHE_ENABLE=1 with code mounted as a volume. Edits will not show up until the container restarts. That includes WordPress on a volume updated with git pull or WP-CLI (admin updates still work), and docker exec ... artisan optimize against a live container.

Fix: restart the container after code changes, or set PHP_OPCACHE_VALIDATE_TIMESTAMPS=1 for the old behavior. Nothing changes with PHP_OPCACHE_ENABLE=0.

- Updated default OPcache settings for production mode in various documentation files, emphasizing the importance of `PHP_OPCACHE_ENABLE` and its impact on performance.
- Added a new guide on PHP OPcache tuning, detailing how to measure and adjust OPcache settings for optimal application performance.
- Clarified the behavior of `PHP_OPCACHE_VALIDATE_TIMESTAMPS` and its implications for development and production environments.
- Adjusted Dockerfiles across multiple variations to reflect new OPcache defaults, including increased memory consumption and maximum accelerated files.
- Improved scripts and entrypoint messages to provide clearer guidance on OPcache usage and best practices for deployment.
@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Sep 10, 2026

Copy link
Copy Markdown

Deploying serversideup-php with  Cloudflare Pages  Cloudflare Pages

Latest commit: 84fe9fa
Status: ✅  Deploy successful!
Preview URL: https://aaa7f3ed.serversideup-php.pages.dev
Branch Preview URL: https://opcache-improvements.serversideup-php.pages.dev

View logs

@github-actions

github-actions Bot commented Sep 10, 2026

Copy link
Copy Markdown

Images for PR #704

Build Test Publish
❌ Build failed (67 of 68 images built) ✅ Every image passed on amd64 and arm64 ⏭️ Not published

No images were published from this run. View the run for details.

All images with sizes

Sizes are compressed, per architecture.

Variation PHP Base OS amd64 arm64 Image
cli 8.5.10 alpine3.23 built, not published
cli 8.5.10 alpine3.24 built, not published
cli 8.5.10 bookworm built, not published
cli 8.5.10 trixie built, not published
cli 8.4.25 alpine3.23 built, not published
cli 8.4.25 alpine3.24 built, not published
cli 8.4.25 bookworm built, not published
cli 8.4.25 trixie built, not published
cli 8.3.33 alpine3.23 built, not published
cli 8.3.33 alpine3.24 built, not published
cli 8.3.33 bookworm built, not published
cli 8.3.33 trixie built, not published
cli 8.2.33 alpine3.23 built, not published
cli 8.2.33 alpine3.24 built, not published
cli 8.2.33 bookworm built, not published
cli 8.2.33 trixie built, not published
fpm 8.5.10 alpine3.23 built, not published
fpm 8.5.10 alpine3.24 built, not published
fpm 8.5.10 bookworm built, not published
fpm 8.5.10 trixie built, not published
fpm 8.4.25 alpine3.23 built, not published
fpm 8.4.25 alpine3.24 built, not published
fpm 8.4.25 bookworm built, not published
fpm 8.4.25 trixie built, not published
fpm 8.3.33 alpine3.23 built, not published
fpm 8.3.33 alpine3.24 built, not published
fpm 8.3.33 bookworm built, not published
fpm 8.3.33 trixie built, not published
fpm 8.2.33 alpine3.23 built, not published
fpm 8.2.33 alpine3.24 built, not published
fpm 8.2.33 bookworm built, not published
fpm 8.2.33 trixie built, not published
fpm-apache 8.5.10 bookworm built, not published
fpm-apache 8.5.10 trixie built, not published
fpm-apache 8.4.25 bookworm built, not published
fpm-apache 8.4.25 trixie built, not published
fpm-apache 8.3.33 bookworm built, not published
fpm-apache 8.3.33 trixie built, not published
fpm-apache 8.2.33 bookworm built, not published
fpm-apache 8.2.33 trixie built, not published
fpm-nginx 8.5.10 alpine3.23 built, not published
fpm-nginx 8.5.10 alpine3.24 built, not published
fpm-nginx 8.5.10 bookworm built, not published
fpm-nginx 8.5.10 trixie built, not published
fpm-nginx 8.4.25 alpine3.23 built, not published
fpm-nginx 8.4.25 alpine3.24 built, not published
fpm-nginx 8.4.25 bookworm built, not published
fpm-nginx 8.4.25 trixie built, not published
fpm-nginx 8.3.33 alpine3.23 built, not published
fpm-nginx 8.3.33 alpine3.24 built, not published
fpm-nginx 8.3.33 bookworm built, not published
fpm-nginx 8.3.33 trixie built, not published
fpm-nginx 8.2.33 alpine3.23 built, not published
fpm-nginx 8.2.33 alpine3.24 built, not published
fpm-nginx 8.2.33 bookworm built, not published
fpm-nginx 8.2.33 trixie built, not published
frankenphp 8.5.10 alpine3.23 built, not published
frankenphp 8.5.10 alpine3.24 built, not published
frankenphp 8.5.10 bookworm built, not published
frankenphp 8.5.10 trixie built, not published
frankenphp 8.4.25 alpine3.23 built, not published
frankenphp 8.4.25 alpine3.24 built, not published
frankenphp 8.4.25 bookworm built, not published
frankenphp 8.4.25 trixie built, not published
frankenphp 8.3.33 alpine3.23 built, not published
frankenphp 8.3.33 alpine3.24 built, not published
frankenphp 8.3.33 bookworm built, not published
frankenphp 8.3.33 trixie ❌ not built

Updated on every push to this PR.

@Abdulmajeed-Jamaan Abdulmajeed-Jamaan Sep 11, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

First of all i truly appreciate the effort behind this PR

I would suggest the following:

This page should be targeted for the person who want to start to use the serversideup docker images for production only.

I would start by calling it "Getting started for production"

Naming it like that Because the production related configs are not strictly bounded to opcache only, it could be opcache, php, caddy, nginx or any production configs that the consumer must check their values before going to production.

For example the PHP_UPLOAD_MAX_FILE_SIZE its really needed to be set on demand set by the consumer to make sure its value proper with his application needs. and so on with other configs.

This page could have the configs grouped into sections like opcache/php/caddy/... etc for proper readibility and navigation.

Also add octane based section to point out the amount of octane workers configs and how to set them properly.

It also can be as Sub page under each image flavor so its scoped accordengly.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also i would suggest adding the following configs to check/tune on demand before going to production as it mostly coupled by the application code and business logic:

  • PHP_DATE_TIMEZONE
  • PHP_MEMORY_LIMIT
  • PHP_MAX_EXECUTION_TIME
  • PHP_UPLOAD_MAX_FILE_SIZE
  • PHP_SESSION_COOKIE_SECURE
  • PHP_REALPATH_CACHE_TTL

| Variable | Default | What it controls |
|----------|---------|------------------|
| `PHP_OPCACHE_VALIDATE_TIMESTAMPS` | `0` | Whether OPcache checks if a file changed. `0` means files are cached until the container restarts. |
| `PHP_OPCACHE_REVALIDATE_FREQ` | `2` | How many seconds between change checks. Ignored unless timestamps are validated. |

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We are focusing on production configs only, if its always ignored, its better to not mention them at all.

And as far as i know the PHP_OPCACHE_VALIDATE_TIMESTAMPS should always remain false in production.

Maybe mention the PHP_OPCACHE_REVALIDATE_FREQ as a not within the the What it controls column, but not as something needed for production (by giving it full row in the table)


## What production mode gives you

When `PHP_OPCACHE_ENABLE=1`, these are the defaults you get:

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Its better to point out that they can be tuned as needed

| `PHP_OPCACHE_INTERNED_STRINGS_BUFFER` | `16` | Megabytes reserved inside that segment for interned strings. |
| `PHP_OPCACHE_MAX_ACCELERATED_FILES` | `20000` | Maximum number of files that can be cached. |
| `PHP_OPCACHE_FORCE_RESTART_TIMEOUT` | `180` | Seconds to wait for a stuck cache restart before OPcache forces it. |
| `PHP_OPCACHE_SAVE_COMMENTS` | `1` | Keep PHPDoc comments in the cache. Required by Doctrine, PHPUnit, and many Laravel packages. |

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I prefer this to be disabled be default for production.

The production defaults should be optimized at maximum for the most common apps, and any thing effects that should be set intentionally and be aware of (not hidden in default configs), also enabling this would not throw any error or anything, the dock blocks will waste a debatable amount of opcache memory space.

  • PHPUnit: is for development"
  • Doctrine: i don't know about symphony but i checked the latest laravel 13 (with AI) nothing in the stock skeleton's production tree needs opcache.save_comments to be true.
  • many Laravel packages: not everyone uses every package.

`PHP_OPCACHE_ENABLE` sets both `opcache.enable` and `opcache.enable_cli`, so CLI commands such as `php artisan` also run with OPcache. This helps long-running CLI processes like queue workers and Horizon.
::

## What production mode gives you

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would relabel this to be Configs to check and tune and guide on each how to set it properly. for example the PHP_OPCACHE_MAX_ACCELERATED_FILES, could be helpful to tell you need to count the amount of php files in production installation to set properly.

Comment on lines +66 to +85
## Measuring what your app needs

Guessing is unnecessary. PHP tells you exactly how full the cache is. Drop this into a route or run it through the web server, since the CLI has its own separate cache:

```php [public/opcache-status.php]
<?php
$status = opcache_get_status(false);

echo 'Cache full: ' . ($status['cache_full'] ? 'yes' : 'no') . PHP_EOL;
echo 'Cached files: ' . $status['opcache_statistics']['num_cached_keys'] . ' of ' . $status['opcache_statistics']['max_cached_keys'] . PHP_EOL;
echo 'Memory used: ' . round($status['memory_usage']['used_memory'] / 1048576) . ' MB' . PHP_EOL;
echo 'Memory free: ' . round($status['memory_usage']['free_memory'] / 1048576) . ' MB' . PHP_EOL;
echo 'Interned strings free: ' . round($status['interned_strings_usage']['free_memory'] / 1048576) . ' MB' . PHP_EOL;
echo 'Out of memory restarts: ' . $status['opcache_statistics']['oom_restarts'] . PHP_EOL;
echo 'Hash restarts: ' . $status['opcache_statistics']['hash_restarts'] . PHP_EOL;
```

::caution
Remove this file before deploying, or protect it. It exposes the full path of every cached file.
::

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I really would like a ready made larave command or a script that prints these measures directly, or even set them.

So it can run in these ways:

  • manually one time to set the values.
  • CI/CD pipelines to check if the settings needs to be increased on every code changes.
  • Container entrypoint to set them on demand with consediration of the available cpu/memory resources.

Comment on lines +87 to +93
What to look for after your application has served real traffic for a while:

- **Cache full is `yes`** or **out of memory restarts** is climbing: raise `PHP_OPCACHE_MEMORY_CONSUMPTION`.
- **Cached files** is close to the maximum or **hash restarts** is climbing: raise `PHP_OPCACHE_MAX_ACCELERATED_FILES`.
- **Interned strings free** is near zero: raise `PHP_OPCACHE_INTERNED_STRINGS_BUFFER`.

Restarts are the thing to avoid. When OPcache runs out of room it throws the entire cache away and recompiles everything, which shows up as a latency spike.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we determine these before serving real traffic ?

@Abdulmajeed-Jamaan Abdulmajeed-Jamaan Sep 11, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

And whats the way to "look for" ? manually ssh to the server or ?

@jaydrogers

Copy link
Copy Markdown
Member Author

@Abdulmajeed-Jamaan You beat me to it 😃

I came back with another pass this morning. I appreciate your feedback.

Take a look at this: https://opcache-improvements.serversideup-php.pages.dev/open-source/docker-php/docs/guide/production-performance-tuning

@jaydrogers
jaydrogers marked this pull request as ready for review September 11, 2026 17:40
@jaydrogers

Copy link
Copy Markdown
Member Author

I have another branch that depends on this being merged into my release branch. I am going to merge this, but I am still open to improving this.

I think this is a good start and we can continue to improve it if needed

@jaydrogers
jaydrogers merged commit ed8141b into release/webserver-improvements-and-fixes Sep 11, 2026
109 of 110 checks passed
@jaydrogers
jaydrogers deleted the opcache-improvements branch September 11, 2026 21:51
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants